﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEditor.Experimental.SceneManagement;
using UnityEditor.SceneManagement;
using UnityEngine;
using XYRendering.Vegetation;
using Assert = UnityEngine.Assertions.Assert;


namespace nTools.PrefabPainter
{
    public class PrefabPainterDataBakerGUI : PrefabPainterEditorExtendsion
    {
        static class UILables
        {
            public static string bakeSettingUndo = "PP(ss extendsion): bake setting undo";
            public static string bakeUnspecifyTerrainError = "未指定地板或地形Game Object!";
            public static string bakeUnSpecifyDataError = "未指定烘焙数据！";
            public static string bakeInvalidDivisionError = "分块数不正确，必须大于1！";
            public static string bakeTerrainUnsupportedError = "不支持的地表类型， 仅支持美术建模地表或者Unity内置Terrain！";
            
            public static GUIContent placingGO = new GUIContent("Placing Game Object", "Objects当前绘制的Game Object");
            public static GUIContent layerMask = new GUIContent("LayerMask", "指定勾选Layers的Objects将被烘焙到Data");
            public static GUIContent maxDivision = new GUIContent("Max Division", "地表最大分块数");
            public static GUIContent division = new GUIContent("Division", "地表划分块数 NXN");
            public static GUIContent heightOffset = new GUIContent("Block Height Offset", "Blocks的高度偏移");
            public static GUIContent includeInactive = new GUIContent("IncludeInactive", "是否包含未激活的Objects");
            public static GUIContent autoHeightAdaption = new GUIContent("Auto Height Adaption", "地表分块自动适配地表高度");
            public static GUIContent usingVGRendererWhenBakedFinish = new GUIContent("Using GPU Instancing When baked Finish", "烘焙完成后自动使用GPU Instancing");
            public static GUIContent removePaintedObjsWhenBakedFinish = new GUIContent("Remove Paint Objects When Baked Finish", "烘焙完成后移除场景中刷好的Objects");
            public static GUIContent data = new GUIContent("Bake To Data", "指定烘焙数据");
            

            public static GUIContent showBounds = new GUIContent("Shou Bounds");
            public static GUIContent boundsColor = new GUIContent("Bounds Color");
            public static GUIContent showBlock = new GUIContent("Show blocks");
            public static GUIContent blockColor = new GUIContent("Block Color");
            public static GUIContent showIndex = new GUIContent("Show Index");
            public static GUIContent indexColor = new GUIContent("Index Color");
            
            public static GUIContent beginPrev = new GUIContent("开始预览", "进入预览模式");
            public static GUIContent endPrev = new GUIContent("结束预览", "退出预览模式");
            public static GUIContent newData = new GUIContent("创建数据", "新建一份新的数据");
            public static GUIContent bakeData = new GUIContent("烘焙数据", "开始烘焙数据");
            public static GUIContent reconstructScene = new GUIContent("重建场景", "从烘焙的数据重建烘焙之前的场景");

            public static string newDataWndTitle = "新建烘焙数据";
        }
        
        
        int _toolId = -1;
        public int toolId => _toolId;
        
        bool _BakeToolFoldout = true;
        bool _PreviewFoldout = true;
        
        bool _IsPreviewMode = false;
        
        Texture2D _ResetIconTexture;
        static int s_MinButtonWidth = 150;
        
        public VGInstancingDataBakeSetting _bakeSetting;
        VGInstancingDataBakePreviewSetting _previewSetting;
        PrefabPainterDataBakePreviewer _previewDrawer;
        public GUIContent CreateTabIcon(PrefabPainter mainEditor)
        {
            return new GUIContent("", mainEditor.LoadGUITexture("BakeTool36x36.png"), "Bake Tool");
        }

        public void OnInit(PrefabPainter mainEditor, int extendedTabId)
        {
            _toolId = extendedTabId;
            var subfix = (EditorGUIUtility.pixelsPerPoint > 1.8f) ? "_HIDPI.png" : ".png";
            _ResetIconTexture = mainEditor.LoadGUITexture("ResetIcon" + subfix);
            _BakeToolFoldout = EditorPrefs.GetBool("nTools.PrefabPainter.ssextendsion.bakeToolFoldout", _BakeToolFoldout);
            _PreviewFoldout = EditorPrefs.GetBool("nTools.PrefabPainter.ssextendsion.previewFoldout", _PreviewFoldout);
            _bakeSetting = new VGInstancingDataBakeSetting();
            _previewSetting = new VGInstancingDataBakePreviewSetting();
        }

        public void OnGUI()
        {
            EditorGUILayout.BeginVertical(EditorStyles.helpBox);
            EditorGUILayout.LabelField("Bake Tool");
            EditorGUILayout.LabelField("把刷好的Objects烘焙成数据，供运行时进行GPU Instancing绘制使用", EditorStyles.wordWrappedMiniLabel);
            EditorGUILayout.EndVertical();
            
            BakeSettingGUI();
            
            PreviewSettingGUI();
            
            BakeGUI();
        }

        public void onToolEnable()
        {
        }


        public void onToolDisable()
        {
            if (_IsPreviewMode)
            {
                TogglePreview(_bakeSetting.placingGO, false);
                _IsPreviewMode = false;
            }
        }

        public void OnDestory(PrefabPainter mainEditor)
        {
            if (_IsPreviewMode)
            {
                TogglePreview(_bakeSetting.placingGO, false);
                _IsPreviewMode = false;
            }
            EditorPrefs.SetBool("nTools.PrefabPainter.ssextendsion.bakeToolFoldout", _BakeToolFoldout);
            EditorPrefs.SetBool("nTools.PrefabPainter.ssextendsion.previewFoldout", _PreviewFoldout);
        }
        
        
        void BakeSettingGUI()
        {
            // Bake Settings
            _BakeToolFoldout = InternalGUI.FoldoutReset(_BakeToolFoldout, () => {}, "Bake Setting", _ResetIconTexture);
            if (_BakeToolFoldout)
            {
                EditorGUI.indentLevel++;
                
                float oldLableWidth = EditorGUIUtility.labelWidth;
                EditorGUIUtility.labelWidth = 300;
                
                EditorGUI.BeginDisabledGroup(_IsPreviewMode);
                SaveIfChange(() => (GameObject)EditorGUILayout.ObjectField(UILables.placingGO, _bakeSetting.placingGO, typeof(GameObject), true), ref _bakeSetting.placingGO) ;
                SaveIfChange(() => InternalGUI.LayerMaskField(UILables.layerMask, _bakeSetting.layerMask), ref _bakeSetting.layerMask);
                SaveIfChange(() => EditorGUILayout.Toggle(UILables.includeInactive, _bakeSetting.includeInactive), ref _bakeSetting.includeInactive);
                SaveIfChange(() => (VGInstancingData)EditorGUILayout.ObjectField(UILables.data, _bakeSetting.data, typeof(VGInstancingData), false), ref _bakeSetting.data);
                Color oldColor = GUI.color;
                GUI.color = Color.yellow;
                SaveIfChange(() => EditorGUILayout.Toggle(UILables.removePaintedObjsWhenBakedFinish, _bakeSetting.removePaintedObjsWhenBakeFinish, EditorStyles.toggle), ref _bakeSetting.removePaintedObjsWhenBakeFinish);
                SaveIfChange(() => EditorGUILayout.Toggle(UILables.usingVGRendererWhenBakedFinish, _bakeSetting.usingVGRendererWhenBakedFinish), ref _bakeSetting.usingVGRendererWhenBakedFinish);
                GUI.color = oldColor;
                EditorGUI.EndDisabledGroup();
                
                EditorGUIUtility.labelWidth = oldLableWidth;

                bool flag = SaveIfChange(() => EditorGUILayout.Toggle(UILables.autoHeightAdaption, _bakeSetting.autoHeightAdaption), ref _bakeSetting.autoHeightAdaption);
                flag |= SaveIfChange(() => (int)Mathf.Max(EditorGUILayout.IntField(UILables.maxDivision, _bakeSetting.maxDivision, GUILayout.MaxWidth(200)), 8), ref _bakeSetting.maxDivision);
                flag |= SaveIfChange(() => EditorGUILayout.FloatField(UILables.heightOffset, _bakeSetting.blockHeightOffset, GUILayout.MaxWidth(200)), ref _bakeSetting.blockHeightOffset);
                flag |= SaveIfChange(() => EditorGUILayout.IntSlider(UILables.division, _bakeSetting.division, 1, _bakeSetting.maxDivision), ref _bakeSetting.division);
                
                if (_IsPreviewMode && flag)
                    _previewDrawer.SetDirty();

               
                EditorGUI.indentLevel--;
            }
        }

        void PreviewSettingGUI()
        {
            _PreviewFoldout = InternalGUI.FoldoutReset(_PreviewFoldout, () => {}, "Preview Setting", _ResetIconTexture);
            if (_PreviewFoldout)
            {
                EditorGUI.indentLevel++;
                SaveIfChange(() => EditorGUILayout.Toggle(UILables.showBounds, _previewSetting.showBounds), ref _previewSetting.showBounds);
                SaveIfChange(() => EditorGUILayout.ColorField(UILables.boundsColor, _previewSetting.boundsColor, GUILayout.MaxWidth(250)), ref _previewSetting.boundsColor);
                SaveIfChange(() => EditorGUILayout.Toggle(UILables.showBlock, _previewSetting.showBlock), ref _previewSetting.showBlock);
                SaveIfChange(() => EditorGUILayout.ColorField(UILables.blockColor, _previewSetting.blockColor, GUILayout.MaxWidth(250)), ref _previewSetting.blockColor);
                SaveIfChange(() => EditorGUILayout.Toggle(UILables.showIndex, _previewSetting.showBlockIndex), ref _previewSetting.showBlockIndex);
                SaveIfChange(() => EditorGUILayout.ColorField(UILables.indexColor, _previewSetting.indexColor, GUILayout.MaxWidth(250)), ref _previewSetting.indexColor);
                EditorGUI.indentLevel--;
            }
        }

        VGInstancingDataBakeError ErrorGUI()
        {
            var e = VGInstancingDataBaker.ChechError(_bakeSetting);
            bool hasError = e != VGInstancingDataBakeError.NoError;
            if (hasError)
            {
                EditorGUILayout.Space(20);
        
                if ((((int)e & (int)VGInstancingDataBakeError.UnSpecifyTerrain)) > 0)
                {
                    EditorGUILayout.HelpBox(UILables.bakeUnspecifyTerrainError, MessageType.Error);
                }
            
                if ((((int)e & ((int)VGInstancingDataBakeError.UnSpecifyBakingData))) > 0)
                {
                    EditorGUILayout.HelpBox(UILables.bakeUnSpecifyDataError, MessageType.Error);
                }
            
                if ((((int)e & (int)VGInstancingDataBakeError.UnvaildDivision)) > 0)
                {
                    EditorGUILayout.HelpBox(UILables.bakeInvalidDivisionError, MessageType.Error);
                }
            
                if ((((int)e & (int)VGInstancingDataBakeError.UnsupportedTerrain)) > 0)
                {
                    EditorGUILayout.HelpBox(UILables.bakeTerrainUnsupportedError, MessageType.Error);
                }
                
                EditorGUILayout.Space(20);
            }
            
            return e;
        }

        public void ExternalUseBeak(out int outOfBlockCount)
        {
            
            var copyBakeSetting = _bakeSetting.Copy();
            copyBakeSetting.data = ScriptableObject.CreateInstance<VGInstancingData>();
            var result = VGInstancingDataBaker.Bake(copyBakeSetting, (p, m) =>
                    EditorUtility.DisplayProgressBar("bake Prefab Painter Data", "processing " + m + " ...", p),
                () => EditorUtility.ClearProgressBar());

            if (result.error == VGInstancingDataBakeError.NoError)
            {
                if (result.outOfBlocksCount > result.bakedVgInstanceCount * 0.1f)
                {
                    outOfBlockCount = result.outOfBlocksCount;
                }
                else
                {
                    outOfBlockCount = 0;
                    if (AssetDatabase.TryGetGUIDAndLocalFileIdentifier(_bakeSetting.data, out var guid, out long _))
                    {
                        _bakeSetting.data = copyBakeSetting.data;
                        var path = AssetDatabase.GUIDToAssetPath(guid);
                        AssetDatabase.DeleteAsset(path);
                        AssetDatabase.CreateAsset(copyBakeSetting.data, path);
                        AssetDatabase.SaveAssets();
                        
                        OnBakeFinished();
                    }
                    else
                    {
                        EditorUtility.DisplayDialog("Save Failed",
                            "Lose original asset location, Please try again", "Ok");
                        GUIUtility.ExitGUI();
                    }
                }
            }
            else
            {
                string msg = "";
                switch (result.error)
                {
                    case VGInstancingDataBakeError.UnsupportedTerrain:
                        msg = "不支持的地表类型，目前仅仅支持美术建模地表或者Unity内置Terain";
                        break;
                    default:
                        msg = "未知内部错误，请通知程序排除BUG";
                        break;
                }

                outOfBlockCount = 10;
                // EditorUtility.DisplayDialog("Bake Failed", msg, "Ok");
                // GUIUtility.ExitGUI();
            }
            
        }
        
        
        void BakeGUI()
        {
            var e = ErrorGUI();
            bool hasError = e != VGInstancingDataBakeError.NoError;
            EditorGUILayout.Space(50);
            EditorGUILayout.BeginHorizontal();
            GUILayout.FlexibleSpace();
            
            // Preview
            EditorGUI.BeginDisabledGroup(hasError);
            if (GUILayout.Button(_IsPreviewMode ? UILables.endPrev : UILables.beginPrev,
                GUILayout.MinWidth(s_MinButtonWidth)))
            {
                _IsPreviewMode = !_IsPreviewMode;
                TogglePreview(_bakeSetting.placingGO, _IsPreviewMode);
            }
            EditorGUI.EndDisabledGroup();
            
            // Bake
            EditorGUI.BeginDisabledGroup(hasError || _IsPreviewMode);
            if(GUILayout.Button(UILables.bakeData, GUILayout.MinWidth(s_MinButtonWidth)))
            {
                    var copyBakeSetting = _bakeSetting.Copy();
                    copyBakeSetting.data = ScriptableObject.CreateInstance<VGInstancingData>();
                    var result = VGInstancingDataBaker.Bake(copyBakeSetting, (p, m) =>
                            EditorUtility.DisplayProgressBar("bake Prefab Painter Data", "processing " + m + " ...", p),
                        () => EditorUtility.ClearProgressBar());

                    if (result.error == VGInstancingDataBakeError.NoError)
                    {
                        if (EditorUtility.DisplayDialog("Bake Result", string.Format(
                            "total instances count: {0}\nbake instance count: {1}\nskip instance count: {2}, for follow reason:\n\t{3} has multiple submesh\n\t{4} outside of any blocks",
                            result.vgInstanceCount, result.bakedVgInstanceCount, result.skipVgInstanceCount,
                            result.multiSubMeshCount, result.outOfBlocksCount), "Save", "Cancel"))
                        {
                            //create new asset to override original data
                            if (AssetDatabase.TryGetGUIDAndLocalFileIdentifier(_bakeSetting.data, out var guid, out long _))
                            {
                                _bakeSetting.data = copyBakeSetting.data;
                                var path = AssetDatabase.GUIDToAssetPath(guid);
                                AssetDatabase.DeleteAsset(path);
                                AssetDatabase.CreateAsset(copyBakeSetting.data, path);
                                AssetDatabase.SaveAssets();
                                
                                OnBakeFinished();
                            }
                            else
                            {
                                EditorUtility.DisplayDialog("Save Failed",
                                    "Lose original asset location, Please try again", "Ok");
                                GUIUtility.ExitGUI();
                            }
                        }
                        GUIUtility.ExitGUI();
                    }
                    else
                    {
                        string msg = "";
                        switch (result.error)
                        {
                            case VGInstancingDataBakeError.UnsupportedTerrain:
                                msg = "不支持的地表类型，目前仅仅支持美术建模地表或者Unity内置Terain";
                                break;
                            default:
                                msg = "未知内部错误，请通知程序排除BUG";
                                break;
                        }
                        EditorUtility.DisplayDialog("Bake Failed", msg, "Ok");
                        GUIUtility.ExitGUI();
                    }

            }
            EditorGUI.EndDisabledGroup();
            
            // New Data
            EditorGUI.BeginDisabledGroup(_IsPreviewMode);
            if (GUILayout.Button(UILables.newData, GUILayout.MinWidth(s_MinButtonWidth)))
            {
                var curScene = EditorSceneManager.GetActiveScene();
                var scenePath = Path.GetDirectoryName(Path.GetFullPath(curScene.path));
                var savePath = EditorUtility.SaveFilePanelInProject(UILables.newDataWndTitle, curScene.name + "_instancingData", "asset", "", scenePath);
                if (!string.IsNullOrEmpty(savePath))
                {
                    VGInstancingData newData = ScriptableObject.CreateInstance<VGInstancingData>();
                    AssetDatabase.CreateAsset(newData, savePath);
                    AssetDatabase.LoadAssetAtPath<VGInstancingData>(savePath);
                    _bakeSetting.data = newData;
                }
            }
            EditorGUI.EndDisabledGroup();
            
            
            // reconstruct paint scene
            bool noTerrainOrNoData = (e & VGInstancingDataBakeError.UnSpecifyTerrain) > 0 
                                    || (e & VGInstancingDataBakeError.UnSpecifyBakingData) > 0;
            EditorGUI.BeginDisabledGroup(_IsPreviewMode || noTerrainOrNoData);
            if (GUILayout.Button(UILables.reconstructScene, GUILayout.MinWidth(s_MinButtonWidth)))
            {
                ReconstructSceneFromData();
            }
            EditorGUI.EndDisabledGroup();


            GUILayout.FlexibleSpace();
            EditorGUILayout.EndHorizontal();
        }

        

        void TogglePreview(GameObject go, bool on)
        {
            if (on)
            {
                Assert.IsNull(_previewDrawer);
                _previewDrawer = go.AddComponent<PrefabPainterDataBakePreviewer>();
                //_previewDrawer.hideFlags = HideFlags.HideAndDontSave;
                _previewDrawer.prevSetting = _previewSetting;
                _previewDrawer.bakeSetting = _bakeSetting;
                _previewDrawer.SetDirty();
            }
            else
            {
                Assert.IsNotNull(_previewDrawer);
                UnityEngine.Object.DestroyImmediate(_previewDrawer);
                _previewDrawer = null;
            }
        }

        Dictionary<string,GameObject> WarmUpPrefabList(VGInstancingData data)
        {
            var dic = new Dictionary<string, GameObject>();
            for (int i = 0; i < data.blocksCount; i++)
            {
                var block = data.blocks[i];
                for (int j  = 0 ; j < block.instancesCount;j++)
                {
                   string name = block.instances[j].name;
                   if(!dic.ContainsKey(name))
                   {
                        var objPrefabs = AssetDatabase.FindAssets($"{name} t:Prefab");
                        GameObject o = null;
                        if (objPrefabs != null)
                        {
                             o = AssetDatabase.LoadAssetAtPath<GameObject>(AssetDatabase.GUIDToAssetPath(objPrefabs[0]));
                        }
                        dic.Add(name,o);
                   }
                }
            }

            return dic;
        }
        
        void ReconstructSceneFromData()
        {
            VGInstancingData data = _bakeSetting.data;
            GameObject terrain = _bakeSetting.placingGO;
            // remove every active group
            RemovePaintedObjects();
            
            // create instance prototype
            GameObject prototype = new GameObject();
            prototype.AddComponent<MeshFilter>();
            prototype.AddComponent<MeshRenderer>();
            Dictionary<int, GameObject> groups = new Dictionary<int, GameObject>();

            var prefabsDic = WarmUpPrefabList(data);
            
            
            for (int i = 0; i < data.blocksCount; i++)
            {
                var block = data.blocks[i];
                for (int j = 0; j < block.instancesCount; j++)
                {
                    var ins = block.instances[j];
                    var prefab = data.prefabs[ins.prefabIndex];
                    GameObject group;
                    if (!groups.TryGetValue(ins.prefabIndex, out group))
                    {
                        group = new GameObject(prefab.groupName);
                        group.transform.SetParent(terrain.transform);
                        group.transform.localPosition = prefab.groupLocalPosition;
                        group.transform.localScale = prefab.groupLocalScale;
                        group.transform.localEulerAngles = prefab.groupLocalRotation;
                        groups[ins.prefabIndex] = group;
                    }
                    // PrefabUtility.InstantiatePrefab(prototype);

                    GameObject insGo = null;
                    if (prefabsDic[ins.name]!=null)
                    {
                        insGo = (GameObject)PrefabUtility.InstantiatePrefab(prefabsDic[ins.name]);
                    }
                    else
                    {
                        insGo = GameObject.Instantiate(prototype);    
                    }
                    
                    insGo.transform.SetParent(group.transform);
                    insGo.name = ins.name;
                    insGo.transform.localPosition = ins.localPosition;
                    insGo.transform.localScale = ins.localScale;
                    insGo.transform.localEulerAngles = ins.localRotation;
                    insGo.GetComponent<MeshFilter>().sharedMesh = prefab.mesh;
                    insGo.GetComponent<MeshRenderer>().sharedMaterial = prefab.material;
                    insGo.layer = ins.layer;
                }
            }
            
            UnityEngine.Object.DestroyImmediate(prototype);
            
        }


        void OnBakeFinished()
        {
            if (_bakeSetting.removePaintedObjsWhenBakeFinish)
            {
                RemovePaintedObjects();
            }

            if (_bakeSetting.usingVGRendererWhenBakedFinish)
            {
                VGInstancingDataRenderer vgRenderer;
                if (!_bakeSetting.placingGO.TryGetComponent<VGInstancingDataRenderer>(out vgRenderer))
                    vgRenderer = _bakeSetting.placingGO.AddComponent<VGInstancingDataRenderer>();

                vgRenderer.enabled = true;
                vgRenderer.bakeData = _bakeSetting.data;
                vgRenderer.Refresh();
            }
        }

        void RemovePaintedObjects()
        {
            
        // p.groupName.Equals( t.gameObject.GetComponent<MeshFilter>().sharedMesh.name)
            
            var allChild = _bakeSetting.placingGO.GetComponentsInChildren<Transform>(true);
            var allGroup = allChild.Where(t => (_bakeSetting.includeInactive || t.gameObject.activeSelf)
                &&  _bakeSetting.data.prefabs.FindIndex(p => t.name.Contains(p.mesh.name) ||p.groupName.Equals(t.name) ) >= 0).ToArray();
            
            for (int i = allGroup.Count() - 1; i >= 0; i--)
            {
                Debug.Log(allGroup[i].gameObject.name);
                UnityEngine.Object.DestroyImmediate(allGroup[i].gameObject);
            }

        }

        bool SaveIfChange<T>(Func<T> drawer, ref T val)
        {
            EditorGUI.BeginChangeCheck();
            T newVale = drawer();
            if (EditorGUI.EndChangeCheck())
            {
                val = newVale;
                return true;
            }
            
            return false;
        }

    }
}